﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Reflection;
using NHibernate.Mappings;
using NHibernate.Mappings.Framework;
using NHibernate.Mappings.Framework.Internal;

namespace NHibernate.Mappings
{
    public class HbmSerializer
    {
        protected static log4net.ILog log = log4net.LogManager.GetLogger(typeof(HbmSerializer).Name);

        public IList<string> Xmls { get; set; }

        private static HbmSerializer _instance = new HbmSerializer();
        public static HbmSerializer Instance { get {
            return _instance; 
        } }

        public static string _dbPrefix;
        public static string DbPrefix {
            get {
                if (!string.IsNullOrEmpty(_dbPrefix)) 
                    return _dbPrefix;

                _dbPrefix = GetdbPrefix();
                return _dbPrefix ?? "";
            }
        }
        
        protected Dictionary<System.Type, string> registeredTypes = new Dictionary<System.Type, string>();
        protected List<LoadedAssembly> registeredAssemblies = new List<LoadedAssembly>();

        protected static string GetdbPrefix() {
            if (!string.IsNullOrEmpty(ConfigurationManager.AppSettings["DbPrefix"]))
                return ConfigurationManager.AppSettings["DbPrefix"];

            return "";
        }

        protected HbmSerializer()
        {
            this.Xmls = new List<string>();
        }

        #region NHibernate configuration

        DateTime assemblyLoadingWaitTime = DateTime.Now;

        public bool AddAssembly(Assembly assembly) {
            // check for already registered assembly
            LoadedAssembly registeredAssembly = registeredAssemblies.FirstOrDefault<LoadedAssembly>(a => a.Hash == assembly.GetHashCode());

            if (registeredAssembly != null)
            {
                if (registeredAssembly.State == AssemblyLoadedState.Loaded)
                {
                    return false;
                }
                else if (registeredAssembly.State == AssemblyLoadedState.Pending)
                {
                    if ((DateTime.Now - registeredAssembly.LoadingStartTime) > new TimeSpan(0, 0, 10))
                    {
                        // check if previous loading was failed
                        registeredAssembly.State = AssemblyLoadedState.NotLoaded;
                    }
                    else
                    {
                        // wait for 2 seconds
                        System.Threading.Thread.Sleep(2000);
                    }

                    //check again
                    return AddAssembly(assembly);
                }
                else if (registeredAssembly.State == AssemblyLoadedState.Failed) {
                    return false;
                }

                // try loading it again
                registeredAssembly.State = AssemblyLoadedState.Pending;
                registeredAssembly.LoadingStartTime = DateTime.Now;
                registeredAssembly.NumberOfFailedLoadingTrials++;

                if (registeredAssembly.NumberOfFailedLoadingTrials > 3)
                {
                    log.Error("Failed to load assembly \"" + assembly.FullName + "\"");
                    registeredAssembly.State = AssemblyLoadedState.Failed;
                    return false;
                }
            }
            else {
                registeredAssemblies.Add(new LoadedAssembly()
                {
                    Hash = assembly.GetHashCode(),
                    State = AssemblyLoadedState.Pending,
                    LoadingStartTime = DateTime.Now,
                    NumberOfFailedLoadingTrials = 0
                });
            }

            try
            {
                // LOADING THE ASSEMBLY
                // Get all types
                List<System.Type> types = new List<System.Type>();
                CollectValidNHibernateEntitiesFromAssembly(assembly, types);

                // Load all models
                ActiveRecordModelCollection models = BuildModels(types);

                GraphConnectorVisitor connectorVisitor = new GraphConnectorVisitor(models);
                connectorVisitor.VisitNodes(models);

                SemanticVerifierVisitor semanticVisitor = new SemanticVerifierVisitor(models);
                semanticVisitor.VisitNodes(models);
                
                // Generate XML and add it to the config
                AddXmlToNHibernateCfg(models);

                registeredAssemblies.First<LoadedAssembly>(a => a.Hash == assembly.GetHashCode()).State = AssemblyLoadedState.Loaded;

                log.Error("\"" + assembly.FullName + "\" was successfuly parsed");

                return true;
            }
            catch (Exception ex) {
                log.Error("Error on parsing assembly \"" + assembly.FullName + "\"", ex);

                registeredAssemblies.First<LoadedAssembly>(a => a.Hash == assembly.GetHashCode()).State = AssemblyLoadedState.Failed;

                return false;
            }
        }

        protected void CollectValidNHibernateEntitiesFromAssembly(Assembly assembly, ICollection<System.Type> list)
        {
            System.Type[] types = GetExportedTypesFromAssembly(assembly);

            foreach (System.Type type in types)
            {
                if (IsNHibernateEntity(type))
                {
                    list.Add(type);
                }
            }
        }

        protected System.Type[] GetExportedTypesFromAssembly(Assembly assembly)
        {
            try
            {
                return assembly.GetExportedTypes();
            }
            catch (Exception ex)
            {
                throw new Exception(
                    "Error while loading the exported types from the assembly: " + assembly.FullName, ex);
            }
        }

        /// <summary>
        /// Return true if the type has a [ActiveRecord] attribute
        /// </summary>
        protected bool IsNHibernateEntity(ICustomAttributeProvider type)
        {
            return type.IsDefined(typeof(NHibernateEntityAttribute), false);
        }

        protected ActiveRecordModelCollection BuildModels(IEnumerable<System.Type> types)
        {
            ActiveRecordModelBuilder builder = new ActiveRecordModelBuilder();

            ActiveRecordModelCollection models = builder.Models;

            foreach (System.Type type in types)
            {
                ActiveRecordModel model = builder.Create(type);

                if (model == null)
                {
                    throw new ActiveRecordException(
                        String.Format("ActiveRecordModel for `{0}` could not be created", type.FullName));
                }

                registeredTypes.Add(type, String.Empty);
            }

            return models;
        }

        protected void AddXmlToNHibernateCfg(ActiveRecordModelCollection models)
        {
            XmlGenerationVisitor xmlVisitor = new XmlGenerationVisitor();
            AssemblyXmlGenerator assemblyXmlGenerator = new AssemblyXmlGenerator();
            foreach (ActiveRecordModel model in models)
            {
                if (!model.IsNestedType && !model.IsDiscriminatorSubClass && !model.IsJoinedSubClass)
                {
                    xmlVisitor.Reset();
                    xmlVisitor.CreateXml(model);

                    String xml = xmlVisitor.Xml;

                    if (xml != String.Empty)
                    {
                        AddXmlString(xml, model);
                    }
                }
            }
        }

        protected void AddXmlString(string xml, ActiveRecordModel model)
        {
            try
            {
                this.Xmls.Add(xml);
            }
            catch (Exception ex)
            {
                throw new ActiveRecordException("Error adding information from class " + model.Type.FullName + " to NHibernate. Check the inner exception for more information", ex);
            }
        }

        #endregion
    }

    public enum AssemblyLoadedState {
        NotLoaded,
        Pending,
        Loaded,
        Failed
    }

    public class LoadedAssembly {
        public LoadedAssembly() {
            this.LoadingStartTime = DateTime.Now;
            this.State = AssemblyLoadedState.Pending;
        }

        public int Hash { get; set; }
        public DateTime LoadingStartTime { get; set; }
        public AssemblyLoadedState State { get; set; }
        public int NumberOfFailedLoadingTrials { get; set; }
    }
}
